package org.hawk.gwt.ppc.introspection.impl;

import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.List;

import org.hawk.gwt.ppc.introspection.IntrospectionException;
import org.hawk.gwt.ppc.introspection.IntrospectionUtils;
import org.hawk.gwt.ppc.introspection.JavaArrayTypeScope;
import org.hawk.gwt.ppc.introspection.JavaGenericTypeScope;
import org.hawk.gwt.ppc.introspection.JavaMethod;
import org.hawk.gwt.ppc.introspection.JavaModifier;
import org.hawk.gwt.ppc.introspection.JavaTypeScope;
import org.hawk.gwt.ppc.introspection.JavaTypeVariable;
import org.hawk.gwt.ppc.parser.BlockDescriptor;
import org.hawk.gwt.ppc.parser.Descriptor;
import org.hawk.gwt.ppc.parser.MethodDescriptor;
import org.hawk.gwt.ppc.parser.ModificatedDescriptor;

/**
 * Implementation for java method for introspection API.
 * 
 * @author alex.bereznevatiy@gmail.com
 *
 */
final class JavaMethodImpl extends AbstractJavaCallableMemberScope<MethodDescriptor> implements JavaMethod {
	/**
	 * Returned by match method if the method doen't match name or parameters.
	 */
	public static final int NO_RELEVANCE = 0;
	
	/**
	 * Returned by match method if the method strictly matches name and parameters.
	 */
	public static final int FULL_RELEVANCE = Integer.MAX_VALUE;
	
	private Method method;
	private List<JavaTypeScope> resolvedParams;

	/**
	 * Constructor for source introspection logic.
	 * @param descriptor
	 * @param parent
	 */
	JavaMethodImpl(MethodDescriptor descriptor, JavaTypeScope parent) {
		init(descriptor, (JavaScopeImpl)parent);
	}

	/**
	 * Constructor for binary introspection logic.
	 * @param method
	 * @param parent
	 */
	JavaMethodImpl(Method method, JavaTypeScope parent) {
		init(null, (JavaScopeImpl)parent);
		this.method = method;
	}
	
	void setModifiers(Descriptor descriptor) {
		if (((JavaTypeScope)getParent()).isInterface()) {
			List<JavaModifier> result = fromStrings(((ModificatedDescriptor)descriptor).getModifiers());
			if (!result.contains(JavaModifier.ABSTRACT)) result.add(JavaModifier.ABSTRACT);
			if (!result.contains(JavaModifier.PUBLIC)) result.add(JavaModifier.PUBLIC);
			setModifiers(result);
		} else {
			super.setModifiers(descriptor);
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.JavaCallableMember#isVarArgs()
	 */
	public boolean isVarArgs() {
		if (method != null) {
			return method.isVarArgs();
		}
		return getDescriptor().isVarArgs();
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.JavaCallableMember#getParametersTypes()
	 */
	public List<JavaTypeScope> getParametersTypes() {
		if (resolvedParams != null) {
			return resolvedParams;
		}
		if (method != null) {
			resolvedParams = getBinaryParametersTypes(method.getParameterTypes());
		} else {
			resolvedParams = getSourceParametersTypes(false);
		}
		return resolvedParams;
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.JavaCallableMember#getGenericParameterTypes()
	 */
	public List<JavaTypeScope> getGenericParameterTypes() {
		if (method != null) {
			return getBinaryParametersTypes(method.getGenericParameterTypes());
		}
		return getSourceParametersTypes(true);
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.JavaParameterizable#getTypeVariables()
	 */
	public List<JavaTypeVariable> getTypeVariables() {
		if (method != null) {
			return GenericHelper.buildTypeVariables(method.getTypeParameters(), this);
		}
		return GenericHelper.buildTypeVariables(getDescriptor().getTypeParameters(), this);
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.JavaMethod#getGenericReturnType()
	 */
	public JavaTypeScope getGenericReturnType() {
		if (method != null) {
			return GenericHelper.buildGenericType(method.getGenericReturnType(), this);
		}
		return GenericHelper.buildGenericType(getDescriptor().getReturnType(), this);
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.JavaMethod#getReturnType()
	 */
	public JavaTypeScope getReturnType() {
		if (method != null) {
			return IntrospectionUtils.getType(method.getReturnType());
		}
		JavaTypeScope result = resolveType(getDescriptor().getReturnType(), getCompilationUnit());
		if (result instanceof JavaGenericTypeScope) {
			return ((JavaGenericTypeScope)result).getRawType();
		}
		return result;
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.impl.AbstractJavaCallableMemberScope#getName()
	 * @see org.hawk.gwt.ppc.introspection.JavaMethod#getName()
	 */
	@Override
	public String getName() {
		if (method != null) {
			return method.getName();
		}
		return getDescriptor().getName();
	}

	/**
	 * @param name
	 * @param arguments
	 * @return relevance between methods
	 *  ({@link #NO_RELEVANCE} if method is not matching and
	 *  {@link #FULL_RELEVANCE} if method is strictly matching)
	 */
	int match(String name, List<JavaTypeScope> arguments) {
		if (name == null || !name.equals(getName()) || arguments == null) {
			return NO_RELEVANCE;
		}
		return new ParametersComparator(arguments, getParametersTypes(), isVarArgs()).getRelevance();
	}

	/* (non-Javadoc)
	 * @see org.hawk.gwt.ppc.introspection.impl.JavaScopeImpl#parseChildren(org.hawk.gwt.ppc.parser.Descriptor)
	 */
	@Override
	void parseChildren(Descriptor descriptor) {
		BlockDescriptor body = ((MethodDescriptor)descriptor).getBody();
		if (body != null) {
			super.parseChildren(body);
		}
	}

	/**
	 * Comparator between arguments.
	 * 
	 * @author alex.bereznevatiy@gmail.com
	 *
	 */
	private static final class ParametersComparator {
		
		private List<JavaTypeScope> arguments;
		private List<JavaTypeScope> parametersTypes;
		private boolean varArgs;

		ParametersComparator(List<JavaTypeScope> arguments,
				List<JavaTypeScope> parametersTypes, boolean varArgs) {
			this.arguments = arguments;
			this.parametersTypes = parametersTypes;
			this.varArgs = varArgs;
		}

		int getRelevance() {
			if (!varArgs && parametersTypes.size() != arguments.size()) {
				return NO_RELEVANCE;
			}
			
			if (!varArgs) {
				return compare(parametersTypes.size());
			}
			int length = parametersTypes.size() - 1;
			int result = compare(length);
			if (result == NO_RELEVANCE) {
				return NO_RELEVANCE;
			}
			return compareVarArgs(parametersTypes.get(length), arguments.subList(length, arguments.size()), result);
		}
		
		private int compareVarArgs(JavaTypeScope varArgParam,
				List<JavaTypeScope> params, int result) {
			if (params.isEmpty()) {
				return result;
			}
			if (!(varArgParam instanceof JavaArrayTypeScope)) {
				throw new IntrospectionException("Internal error: Bad var args parameter");
			}
			int relevance;
			if (params.size() == 1 && (relevance = compare(params.get(0), varArgParam, result)) > 0) {
				return relevance;
			}
			varArgParam = ((JavaArrayTypeScope)varArgParam).getComponentType();
			
			for (JavaTypeScope type : params) {
				result = compare(type, varArgParam, result);
				if (result == NO_RELEVANCE) {
					return NO_RELEVANCE;
				}
			}
			return result;
		}

		private int compare(int length) {
			Iterator<JavaTypeScope> args = arguments.iterator();
			Iterator<JavaTypeScope> params = parametersTypes.iterator();
			int result = FULL_RELEVANCE;
			for (int i = 0; i < length; i++) {
				result = compare(args.next(), params.next(), result);
				if (result == NO_RELEVANCE) {
					return NO_RELEVANCE;
				}
			}
			return result;
		}
		
		private int compare(JavaTypeScope from, JavaTypeScope to, int oldResult) {
			if (from instanceof JavaGenericTypeScope) {
				from = ((JavaGenericTypeScope)from).getRawType();
			}
			if (to instanceof JavaGenericTypeScope) {
				to = ((JavaGenericTypeScope)to).getRawType();
			}
			if (from.equals(to)) {
				return oldResult;
			}
			if (to.getName().equals(Object.class.getName())) {
				return oldResult - 100;
			}
			if (from.isPrimitive() || from.isPrimitiveWrapper() ||
					to.isPrimitive() || to.isPrimitiveWrapper()) {
				return comparePrimitives(from, to, oldResult);
			}
			if (to.isAssignableFrom(from)) {
				return oldResult - 100;
			}
			return NO_RELEVANCE;
		}

		private int comparePrimitives(JavaTypeScope from, JavaTypeScope to, int oldResult) {
			if (from.isPrimitiveWrapper() && (to.isPrimitiveWrapper() || 
					to.getName().equals(Number.class.getName()))) {
				return to.isAssignableFrom(from) ? oldResult - 100 : NO_RELEVANCE;
			}
			if ((!from.isPrimitive() && !from.isPrimitiveWrapper()) ||
					(!to.isPrimitive() && !to.isPrimitiveWrapper())) {
				return NO_RELEVANCE;
			}
			if (to.getName().equals(Number.class.getName())) {
				return PrimitiveTypeScopes.NUMBERS.contains(from) ? oldResult - 100 : NO_RELEVANCE;
			}
			if (from.isPrimitive() && to.isPrimitive()) {
				return comparePurePrimitives(from, to, oldResult);
			}
			String unwrapedTo = to.getName();
			if (to.isPrimitiveWrapper()) {
				unwrapedTo = PrimitiveTypeScopes.WRAPPERS.get(unwrapedTo);
			}
			String unwrapedFrom = from.getName();
			if (from.isPrimitiveWrapper()) {
				unwrapedFrom = PrimitiveTypeScopes.WRAPPERS.get(unwrapedFrom);
			}
			return unwrapedFrom.equals(unwrapedTo) ? oldResult - 10 : NO_RELEVANCE;
		}

		private int comparePurePrimitives(JavaTypeScope from, JavaTypeScope to, int oldResult) {
			return PrimitiveTypeScopes.ASSIGNMENTS.get(to.getName()).contains(from.getName()) ? oldResult - 100 : NO_RELEVANCE;
		}
	}
}
